﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Text.RegularExpressions;
using JetBrains.Application;
using JetBrains.ProjectModel;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.CSharp;
using JetBrains.ReSharper.Psi.CSharp.Impl;
using JetBrains.ReSharper.Psi.CSharp.Parsing;
using JetBrains.ReSharper.Psi.CSharp.Tree;
using JetBrains.ReSharper.Psi.ExtensionsAPI.Tree;
using JetBrains.ReSharper.Psi.Impl.PsiManagerImpl;
using JetBrains.ReSharper.Psi.Tree;
using JetBrains.Util;

namespace Neovolve.Extensibility.ReSharper
{
    /// <summary>
    /// The <see cref="NodeProcessor"/>
    /// class is used to resolve and update nodes that are formattable by the <see cref="ValueTypeAliasFormatter"/>.
    /// </summary>
    internal static class NodeProcessor
    {
        /// <summary>
        /// Ensures the namespace exists.
        /// </summary>
        /// <param name="file">The file to check.</param>
        /// <param name="qualifiedNamespaceName">Name of the qualified namespace.</param>
        public static void EnsureNamespaceExists(ICSharpFile file, String qualifiedNamespaceName)
        {
            Boolean namespaceExists = false;

            // Loop through the existing imports
            for (Int32 index = 0; index < file.Imports.Count; index++)
            {
                IUsingDirective directive = file.Imports[index];

                IUsingNamespaceDirective namespaceDirective = directive as IUsingNamespaceDirective;

                // We we could convert the type, skip to the next item
                if (namespaceDirective == null
                    || namespaceDirective.ImportedNamespace == null)
                {
                    continue;
                }

                // Check if the using directive is the one we are looking for
                if (namespaceDirective.ImportedNamespace.QualifiedName == qualifiedNamespaceName)
                {
                    // The namespace is already imported.
                    namespaceExists = true;

                    break;
                }
            }

            // Check if the namespace already exists
            if (namespaceExists == false)
            {
                // We need to add the namespace

                // Create the new using statement
                IUsingDirective directive =
                    CSharpElementFactory.GetInstance(file.GetProject()).CreateUsingDirective(
                        qualifiedNamespaceName, null);

                // Add it to the file
                UsingUtil.AddImportTo(file, directive);
            }
        }

        /// <summary>
        /// Resolves the formattable nodes.
        /// </summary>
        /// <param name="node">The node to search.</param>
        /// <param name="rangeMarker">The range marker.</param>
        /// <param name="option">The option.</param>
        /// <param name="psiManager">The psi manager.</param>
        /// <returns>A <see cref="Collection{T}" /> value.</returns>
        public static Collection<ITreeNode> ResolveFormattableNodes(
            ITreeNode node, IPsiRangeMarker rangeMarker, TypeAliasFormat option, PsiManagerImpl psiManager)
        {
            Collection<ITreeNode> nodes = new Collection<ITreeNode>();

            // Resolve the nodes
            ResolveFormattableNodesInternal(node, rangeMarker, option, psiManager, nodes);

            return nodes;
        }

        /// <summary>
        /// Updates the type usage.
        /// </summary>
        /// <param name="node">The node to update.</param>
        /// <param name="option">The option.</param>
        public static void UpdateTypeUsage(ITreeNode node, TypeAliasFormat option)
        {
            // create AST fragment for type usage
            IProject project = node.GetProject();
            ITreeNode newNode;

            if (node is IDocCommentNode)
            {
                // The comment may contain multiple type definitions that need to be updated
                IDocCommentNode commentNode = node as IDocCommentNode;
                StringBuilder builder = new StringBuilder("///");
                Capture lastCapture = null;
                String commentText = commentNode.CommentText;

                // Loop through each cref capture in the node
                foreach (Capture capture in NodeAnalyser.GetCrefCaptures(commentNode))
                {
                    Int32 previousEndIndex = 0;

                    if (lastCapture != null)
                    {
                        previousEndIndex = lastCapture.Index + lastCapture.Length;
                    }

                    if (capture.Index - 1 > previousEndIndex)
                    {
                        // Add the text prior to this capture to the builder
                        builder.Append(commentText.Substring(previousEndIndex, capture.Index - previousEndIndex));
                    }

                    // Process this capture
                    CrefAttributeDescriptor descriptor = new CrefAttributeDescriptor(capture.Value);

                    for (Int32 index = 0; index < descriptor.Length; index++)
                    {
                        if (descriptor[index].PartType != CrefPartTypes.Type)
                        {
                            // Add the delimiter to the builder
                            builder.Append(descriptor[index]);
                        }
                        else
                        {
                            TypeAliasFormat suggestion = NodeAnalyser.GetFormatTypeSuggestion(descriptor[index].Part, option);

                            if (suggestion != TypeAliasFormat.Ignore)
                            {
                                String newTypeName = NodeAnalyser.GetNewTypeName(descriptor[index].Part, option);

                                builder.Append(newTypeName);
                            }
                            else
                            {
                                builder.Append(descriptor[index]);
                            }
                        }
                    }

                    lastCapture = capture;
                }

                // Check if there is a tail on the comment
                if (lastCapture != null
                    && commentText.Length > lastCapture.Index + lastCapture.Length)
                {
                    builder.Append(commentText.Substring(lastCapture.Index + lastCapture.Length));
                }

                String newCommentValue = builder.ToString();

                newNode = CSharpTokenType.END_OF_LINE_COMMENT.Create(new StringBuffer(newCommentValue), 0, newCommentValue.Length);
            }
            else
            {
                String currentTypeName = node.GetText();
                String newTypeName = NodeAnalyser.GetNewTypeName(currentTypeName, option);

                // Create a new node depending of the original node type
                if (node is IReferenceExpressionNode
                    || node is ITypeUsageNode)
                {
                    newNode = CSharpElementFactory.GetInstance(project).CreateTypeUsageNode(newTypeName);
                }
                else if (node is IDeclaredTypeUsageNode)
                {
                    newNode = CSharpElementFactory.GetInstance(project).CreateDeclaredTypeUsageNode(newTypeName);
                }
                else if (node is IPredefinedTypeExpressionNode)
                {
                    // Ensure that we have the full CLR type name to create the reference expression
                    String clrTypeName = currentTypeName;

                    // Flip the type name as required
                    if (option == TypeAliasFormat.ConvertFromAlias)
                    {
                        clrTypeName = newTypeName;
                    }

                    IReferenceExpression expression =
                        CSharpElementFactory.GetInstance(project).CreateReferenceExpression(clrTypeName);

                    if (expression == null)
                    {
                        return;
                    }

                    IDeclaredType declaredType = GetDeclaredType(project, newTypeName);

                    if (declaredType != null)
                    {
                        // Tell ReSharper that this type usage points to System.String for example, so ReSharper core can insert using directives, or something else 
                        ITypeElement typeElement = declaredType.GetTypeElement();

                        if (typeElement != null)
                        {
                            expression.Reference.BindTo(typeElement);
                        }
                    }

                    newNode = expression.ToTreeNode();
                }
                else
                {
                    return;
                }
            }

            if (newNode == null)
            {
                // There is no node to work with
                return;
            }

            // replace AST fragment in actual file
            using (WriteLockCookie.Create(node.Parent.IsPhysical()))
            {
                ModificationUtil.ReplaceChild(node, newNode);
            }
        }

        /// <summary>
        /// Gets the type of the declared.
        /// </summary>
        /// <param name="module">The module.</param>
        /// <param name="newTypeName">New name of the type.</param>
        /// <returns>A <see cref="IDeclaredType"/> value.</returns>
        private static IDeclaredType GetDeclaredType(IModule module, String newTypeName)
        {
            PredefinedType predefinedType = new PredefinedType(module);

            switch (newTypeName)
            {
                case "object":
                case "Object":

                    return predefinedType.Object;

                case "char":
                case "Char":

                    return predefinedType.Char;

                case "string":
                case "String":

                    return predefinedType.String;

                case "bool":
                case "Boolean":

                    return predefinedType.Bool;

                case "byte":
                case "Byte":

                    return predefinedType.Byte;

                case "short":
                case "Int16":

                    return predefinedType.Short;

                case "int":
                case "Int32":

                    return predefinedType.Int;

                case "long":
                case "Int64":

                    return predefinedType.Long;

                case "ushort":
                case "UInt16":

                    return predefinedType.Ushort;

                case "uint":
                case "UInt32":

                    return predefinedType.Uint;

                case "ulong":
                case "UInt64":

                    return predefinedType.Ulong;

                case "double":
                case "Double":

                    return predefinedType.Double;

                case "decimal":
                case "Decimal":

                    return predefinedType.Decimal;

                case "float":
                case "Single":

                    return predefinedType.Float;

                case "sbyte":
                case "SByte":

                    return predefinedType.Sbyte;

                default:

                    return null;
            }
        }

        /// <summary>
        /// Resolves the formattable nodes.
        /// </summary>
        /// <param name="node">The node to check.</param>
        /// <param name="rangeMarker">The range marker.</param>
        /// <param name="option">The option.</param>
        /// <param name="psiManager">The psi manager.</param>
        /// <param name="nodesToChange">The nodes to change.</param>
        /// <returns>An <see cref="ITreeNode"/> instance.</returns>
        private static ITreeNode ResolveFormattableNodesInternal(
            ITreeNode node,
            IPsiRangeMarker rangeMarker,
            TypeAliasFormat option,
            PsiManagerImpl psiManager,
            ICollection<ITreeNode> nodesToChange)
        {
            // Check if the node falls within the range or the document if there isn't a range
            if (((rangeMarker == null) || !rangeMarker.IsValid)
                || rangeMarker.DocumentRange.Contains(node.GetDocumentRange()))
            {
                // Get the format suggestion for this node
                TypeAliasFormat suggestion = NodeAnalyser.GetFormatTypeSuggestion(node, option);

                // Add the node to the change list if the suggestion is the same as the option specified
                if (suggestion == option)
                {
                    nodesToChange.Add(node);
                }
            }

            // Recurse through all the siblings and child nodes
            ITreeNode child = node.FirstChild;

            if (child == null)
            {
                return node;
            }

            // Loop through the siblings and resolve them
            while (true)
            {
                child = ResolveFormattableNodesInternal(child, rangeMarker, option, psiManager, nodesToChange);

                if (psiManager.IsCurrentTransactionFailed)
                {
                    return node;
                }

                if (child == null)
                {
                    return node;
                }

                ITreeNode nextSibling = child.NextSibling;

                if (nextSibling == null)
                {
                    return node;
                }

                child = nextSibling;
            }
        }
    }
}